Mosquitto(MQTTS)サーバをACMによるサーバ証明書で構築してみました

Mosquitto(MQTTS)サーバをACMによるサーバ証明書で構築してみました

MosquittoのMQTTS(8883)構築例として、オレオレ証明書を使用したものがよく紹介されていますが、本記事では、AWS Certificate Managerによる正規の証明書での構築要領を紹介させて頂きます。
Clock Icon2024.12.31

1 はじめに

製造ビジネステクノロジー部の平内(SIN)です。

メッセージブローカーのMosquittoをMQTTS(TLS:ポート8883)で構築する際は、サーバ証明書が必要となりますが、多くの場合、自己署名証明書(オレオレ証明書)を使用する方法が紹介されています。

今回は、正規のドメイン名で、ACM(AWS Certificate Manager)で発行した証明書を使用して構築する手順を試してみました。

001

002

方法としては、Mosquittoの手前にNLBを配置し、ここでTLSを終端しています。Mosquitto本体は、TLS無しのMQTT:ポート1883で起動しています。

ACMを使用する場合、秘密鍵の管理や、証明書の更新は必要なくなります。
また、独自に作成したサーバ証明書では、これを検証するためのCA証明書もクライアント側に配置することになりますが、パブリックな名前を使用するので、これも必要なくなります。

TLSでは、そのバージョンや、対応暗号方式が問題になることがありますが、今回の方式では、終端がNLBとなるため、Mosquittoの仕様にはまったく依存せず、ELBのセキュリティポリシーで対応することになります。

参考:Network Load Balancer のセキュリティポリシー

2 構築

図のような構成で、Route53及び、ACM以外の部分をCDKとして作成しましたので、そのコードを使用して作業を進めさせていただきます。

003

https://github.com/furuya02/mqtt-attach-acm-to-nlb

(1) ACMによる証明書作成

ここでは、サーバ名を「mqtt.alexa-dev.tokyo」としています。

ドメインを同一アカウント(Route53)で管理している場合、ACMのコンソールから、証明書をリクエスト / パブリック証明書をリクエスト / DNS検証 / Route53でレコードを作成 の手順で数十秒で作成できてしまいます。

参考:AWS Certificate Manager でのパブリック証明書のリクエスト
参考:AWS Certificate Manager パブリック証明書のドメインの所有権を検証する

004

証明書の作成が完了したら、ARNをコピーしておきます。

(2) CDKデプロイ

Githubからコードをcloneします

% git clone https://github.com/furuya02/mqtt-attach-acm-to-nlb.git
% cd mqtt-attach-acm-to-nlb

cdk/cdk.jsonを開いて、下記の2つを編集します。

key value
accountId デプロイするアカウントID
certificateArn 先に作成した証明書のARN
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
  "watch": {
  },
  "context": {
    "projectName": "mqtt-attach-acm-to-nlb",
    "accountId": "xxxxxxxxxxxx",
    "vpcCidr": "10.0.0.0/16",
    "taskCpu": 256,
    "taskMemory": 512,
    "desiredCount": 0,
    "natGateways": 0,
    "certificateArn": "arn:aws:acm:ap-northeast-1:xxxxxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",

cdkフォルダに移動して、CDKをdeployします。
この時点では、「"desiredCount": 0」となっているため、起動するタスク数は、0個となります。

% cd cdk
% npm install
% cdk diff mqtt-attach-acm-to-nlb-stack -c tag=latest
% cdk deploy mqtt-attach-acm-to-nlb-stack -c tag=latest
% cd ..

EC2のコンソールから、deployされたロードバランサーを確認し、DNS名をコピーしておきます。

005

(3) DockerイメージのPush

下記のコマンドで、Dockerイメージがビルドされ、ECRにプッシュされます。

% node deploy.js latest

docker push 123456781234.dkr.ecr.ap-northeast-1.amazonaws.com/mqtt-attach-acm-to-nlb-repo:latest
finish.

(4) Route53へのレコード登録(Aレコード Alias使用)

下記の内容で、レコードを追加します。

項目
レコードタイプ A レコード
エイリアス 有効
レコード名 mqtt(mqtt.ドメイン名)がサーバのホスト名となります
エンドポイント Network Load Blancerへのエリアス
リージョン ap-noetheast-1
Network Load Blancer 先に作成したNLBのDNS名を選択

006

レコードの追加が完了すると、サーバ名でも、NLBのDNS名でも、同じIPアドレスが引けるようになります。

% dig mqtt-attach-acm-to-nlb-nlb-5429fb2fca447d42.elb.ap-northeast-1.amazonaws.com

;; ANSWER SECTION:
mqtt-attach-acm-to-nlb-nlb-5429fb2fca447d42.elb.ap-northeast-1.amazonaws.com. 60 IN A 43.206.49.94

% dig mqtt.alexa-dev.tokyo

;; ANSWER SECTION:
mqtt.alexa-dev.tokyo.	60	IN	A	43.206.49.94

※ エイリアスが利用できない場合は、CNAME登録となります

(5) タスク起動

cdk/cdk.jsonを編集して、desiredCountの0を1に変更して、再度deployします。

{
  "app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
  "watch": {
  },
  "context": {
    "desiredCount": 1,
% cd cdk
% cdk deploy mqtt-attach-acm-to-nlb-stack -c tag=latest
% cd ..

deployが完了すると、ESCの「タスク」タブで、1つの実行中タスクが確認できます。

007

ターゲットグループでは、ヘルスチェックが、Healthyとなっていることを確認できます。

008

3 動作確認

(1) MQTTクライアントによるPub/Sub

mosquittoのクライアントで、トピック/test をSubscribeしておきます。

% mosquitto_sub -p 8883 -h mqtt.alexa-dev.tokyo -t /test --tls-version tlsv1.2
HELLO

同じトピックに対してメッセージをPublishすると、Subscribe側で到着していることが確認できます。

% mosquitto_pub -p 8883 -h mqtt.alexa-dev.tokyo -t /test  -m "HELLO" --tls-version tlsv1.2

-p 8883 を指定することでTLS通信となっています。サーバ名は、パブリックなものなので、CA証明書を指定したりする必要はありません。

(2) ECS Execによるログ確認

ECS Execでログインすると、/mosquitto/log/mosquitto.logでログを確認できます。

% aws ecs execute-command --region ap-northeast-1 --cluster mqtt-attach-acm-to-nlb-cluster --task ca4cfefe874543028c2f3d48516c5d9e --container mqtt-attach-acm-to-nlb-ServiceTaskContainerDefinition --interactive --command "/bin/sh"

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.

Starting session with SessionId: ecs-execute-command-gh6gvpla4n7rvy9qqvuufy6vae
/ # vi /mosquitto/log/mosquitto.log

【起動時のログ】
1735619608: mosquitto version 2.0.20 starting
1735619608: Config loaded from /mosquitto/mosquitto.conf.
1735619608: Opening ipv4 listen socket on port 1883.
1735619608: Opening ipv6 listen socket on port 1883.
1735619608: mosquitto version 2.0.20 running

【Subscribeのログ】
1735620481: New connection from 10.0.0.90:12989 on port 1883.
1735620481: New client connected from 10.0.0.90:12989 as auto-851B7011-5703-58AC-807C-D1F38BA6A18D (p2, c1, k60).
1735620481: No will message specified.
1735620481: Sending CONNACK to auto-851B7011-5703-58AC-807C-D1F38BA6A18D (0, 0)
1735620481: Received SUBSCRIBE from auto-851B7011-5703-58AC-807C-D1F38BA6A18D
1735620481: 	/test (QoS 0)
1735620481: auto-851B7011-5703-58AC-807C-D1F38BA6A18D 0 /test
1735620481: Sending SUBACK to auto-851B7011-5703-58AC-807C-D1F38BA6A18D

【Publishのログ】
1735620485: New client connected from 10.0.0.90:51004 as auto-36D04964-20DB-8768-C557-90979FAF1860 (p2, c1, k60).
1735620485: No will message specified.
1735620485: Sending CONNACK to auto-36D04964-20DB-8768-C557-90979FAF1860 (0, 0)
1735620485: Received PUBLISH from auto-36D04964-20DB-8768-C557-90979FAF1860 (d0, q0, r0, m0, '/test', ... (5 bytes))
1735620485: Sending PUBLISH to auto-851B7011-5703-58AC-807C-D1F38BA6A18D (d0, q0, r0, m0, '/test', ... (5 bytes))
1735620485: Received DISCONNECT from auto-36D04964-20DB-8768-C557-90979FAF1860
1735620485: Client auto-36D04964-20DB-8768-C557-90979FAF1860 disconnected.

NLBは、接続元のIPアドレスを保持していないので、NLBからポート1883に対する接続となっていることが分かります。

4 削除

destroyで、CDKで作成されたリソースは、削除されます。

% cdk destroy mqtt-attach-acm-to-nlb-stack -c tag=latest
tag: latest
Are you sure you want to delete: mqtt-attach-acm-to-nlb-stack (y/n)? y

mqtt-attach-acm-to-nlb-stack: destroying... [1/1]

 ✅  mqtt-attach-acm-to-nlb-stack: destroyed

%

destroyで残ってしまう、ECRリポジトリ及びロググループについては、手動で削除してください。

009
010

また、元々手動で作成したACMの証明書や、Route 53のリソースについても、削除を忘れないでください。

5 補足説明

以下、一部補足の説明をさせてください。

(1) Dockerの内容

Dockerのイメージは、下記のファイルでbuildされています。

ベースイメージは、eclipse-mosquittoであり、mosquittoの設定ファイルは、別途作成したもの(後述)を使用しています。

Dockerfile

FROM eclipse-mosquitto

COPY mosquitto.conf /mosquitto/mosquitto.conf
COPY run.sh /run.sh
RUN chmod +x /run.sh
CMD /run.sh

run.sh

echo "Starting mosquitto"
/usr/sbin/mosquitto -c /mosquitto/mosquitto.conf -v

(2) Mosquittoの設定ファイル

設定ファイルは、以下のとおりです。listenerで1883を指定することで、TLS無しで起動します。

mosquitto.config

listener 1883

allow_anonymous true

log_dest file /mosquitto/log/mosquitto.log
log_type all

connection_messages true

各種モードでの、必要な設定は以下のような感じです。

設定値 MQTT MQTTS MQTTS相互認証 備考
listener 1883 8883 8883 設定がない場合localhostのみ受け付ける
keyfile 必要 必要 サーバ秘密鍵
certfile 必要 必要 サーバ証明書
cafile 必要 クライアントの証明書検証用
require_certificate true クライアント証明書を要求

※ allow_anonymous true (パスワード認証)

(3) NLBのTLS終端

CDKにおいて、NLBのTLS終端は、protocolelbv2.Protocol.TLSを指定して、certificatesに証明書を設置することで行います。

// TLSでListenする場合
const nlbListener = nlbForApp.addListener(`nlbListener`, {
    protocol: elbv2.Protocol.TLS,
    port: 8883,
    certificates: [
        {
            certificateArn: certificateArn,
        },
    ],
});

参考までに、単なるTCPのリスナーであれば、下記のようになります。

// TLSなしでListenする場合
const nlbListener = nlbForApp.addListener(`nlbListener`, {
   port: 1883,
});

6 最後に

今回は、ACMで発行した証明書で、MosquittoによるMQTTSサーバを構築してみました。

基本的にMQTTメッセージブローカーは、IoT Coreが利用できますが、既存システムの移行時に、IoT Coreでは、対応しきれず、Mosquittoで構築する場面もあるようです。

このような時に、証明書をACMで運用できれば、僅かでも管理面の負担が下がるかも知れません。

なお、ELBのTLS終端は、クライアント認証の機能が無いため、今回の仕組みは、TLS相互認証に対応できないことにご注意ください。(相互認証は、MosquittoでTLS終端するしかない)
ただし、パスワード認証は、アプリ層なので、利用可能なはずです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.